前言

​ 在基于Vue 项目的开发过程中,有时候需要引用一些全局文件(定义了样式变量、方法等),这么做是为了增加复用性,提高开发和多人协作的效率。如果是小型或者引用数量比较少的项目,这种全局文件可以直接进行引用;但是在大型项目中,如果每一个需要到的地方都要 import 一次,无疑会增加项目的耦合程度,一旦全局文件的维护者更改了文件名或路径,那便会产生灾难性的影响,影响面非常广。

​ 对于大型的项目,我们还是希望关注点分离,让开发业务代码的同学只关心调用。Vue-CLI3 生成的项目中,官方在文档中提供了几种通过修改 Webpack Loader 配置引入全局文件的方法,分别为:

  • 通过 chainWebpack 选项(style-resources-loader);

  • 通过 Vue-CLI Plugin(vue-cli-plugin-style-resources-loader);

  • 通过 css.loaderOptions 选项;

    下面我们以 SCSS 为例,介绍下这几种引入方法;同时以源码的角度,看看导入全局样式文件后,脚手架内部发生了什么。

chainWebpack 选项

vue.config.js 中提供了直接修改 Webpack 配置的选项,主要是通过 webpack-chain 这个库实现的,它提供了抽象化的链式调用方法并生成配置,可以通过该选项,向预处理器 Loader 导入全局文件。这里主要是通过 style-resources-loader 的 options.patterns 选项,向 Webpack 传递全局文件,先看一段示例代码:

​ Vue-CLI3 中样式相关配置的生成,主要位于 @vue/cli-service/lib/config/css.js 中,该文件的作用主要是生成样式相关的配置,并在 @vue/cli-service/lib/Service.js 中被调用,以下是部分关键源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
function createCSSRule (lang, test, loader, options) {
const baseRule = webpackConfig.module.rule(lang).test(test)

// rules for <style lang="module">
const vueModulesRule = baseRule.oneOf('vue-modules').resourceQuery(/module/)
applyLoaders(vueModulesRule, true)

// rules for <style>
const vueNormalRule = baseRule.oneOf('vue').resourceQuery(/\?vue/)
applyLoaders(vueNormalRule, false)

// rules for *.module.* files
const extModulesRule = baseRule.oneOf('normal-modules').test(/\.module\.\w+$/)
applyLoaders(extModulesRule, true)

// rules for normal CSS imports
const normalRule = baseRule.oneOf('normal')
applyLoaders(normalRule, modules)

function applyLoaders (rule, modules) {
...
}
}

createCSSRule('css', /\.css$/)

createCSSRule('postcss', /\.p(ost)?css$/)

createCSSRule('scss', /\.scss$/, 'sass-loader', loaderOptions.sass)

createCSSRule('sass', /\.sass$/, 'sass-loader', Object.assign({
indentedSyntax: true
}, loaderOptions.sass))

createCSSRule('less', /\.less$/, 'less-loader', loaderOptions.less)

createCSSRule('stylus', /\.styl(us)?$/, 'stylus-loader', Object.assign({
preferPathResolver: 'webpack'
}, loaderOptions.stylus))

​ 在该文件中,CSS 相关的规则会先在 createCSSRule 这个方法被生成,它支持传入四个参数,分别为:1. 创建的规则名;2. 匹配特定的正则;3. 使用的自定义 Loader;4. 对应的Loader 配置。

​ 在函数内部,上文提到的 vue-modules、vue、normal-modules、normal 四种嵌套的匹配规则,则是对应了几种样式被引用的方式,这里使用的是 Rule.oneOf 这个 API ,所以匹配的优先级是从上往下降级;在规则被创建完以后,则会调用 applyLoaders 这个方法,我们对其简单地解析下(省略部分逻辑):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
function applyLoaders (rule, modules) {

// 是否提取 CSS,shouldExtract 变量在函数外部被定义
// 默认只在生产环境下使用 CSS 提取,这将便于你在开发环境下进行热重载
if (shouldExtract) {
rule
.use('extract-css-loader').loader(require('mini-css-extract-plugin').loader)
.options({
publicPath: cssPublicPath
})
} else {
rule.use('vue-style-loader').loader('vue-style-loader').options({
sourceMap,
shadowMode
})
}

// loaderOptions 来源于 vue.config.js 中的 css.loaderOptions 选项

// 定义传递给 css-loader 的选项
const cssLoaderOptions = Object.assign({
sourceMap,
importLoaders: (
1 + // stylePostLoader injected by vue-loader
(hasPostCSSConfig ? 1 : 0) +
(needInlineMinification ? 1 : 0)
)
}, loaderOptions.css)

// 是否使用 CSS Modules,默认是不使用
// loaderOptions.css 中若有传入该选项,localIdentName、modules 在这里会被自定义
if (modules) {
const {
localIdentName = '[name]_[local]_[hash:base64:5]'
} = loaderOptions.css || {}
Object.assign(cssLoaderOptions, {
modules,
localIdentName
})
}

// 使用 CSS Loader,传递合并后的配置选项
rule.use('css-loader').loader('css-loader').options(cssLoaderOptions)

// 这一步的目的是为了确保构建生产版本时,配置项选择不提取CSS时也能被优化
if (needInlineMinification) {
// cssnano 是 PostCSS 的CSS优化和分解插件
rule.use('cssnano').loader('postcss-loader').options({
sourceMap,
plugins: [require('cssnano')(cssnanoOptions)]
})
}

// 检查项目是否有 Postcss 配置
if (hasPostCSSConfig) {
rule.use('postcss-loader').loader('postcss-loader').options(Object.assign({
sourceMap
}, loaderOptions.postcss))
}

// 预处理区相关的设置会运用在此
// 如: createCSSRule('scss', /\.scss$/, 'sass-loader', loaderOptions.sass)
if (loader) {
rule.use(loader).loader(loader).options(Object.assign({ sourceMap }, options))
}
}

​ 在 CSS Modules 相关的配置中,只有在 vue.config.js 中将 css.modules 设置为 true,或者按照 Vue-CLI 约定的方式进行引入才会被开启。

​ 可以看到,createCSSRule 方法已经将大部分关于样式的规则预先创建好,其内部 applyLoaders 方法则会对配置项进行创建与合并,并传递至对应的 Loader。

​ 把代码加上注释便于理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const path = require('path')

module.exports = {
chainWebpack: config => {
// 定义四类需要需要匹配的规则
const types = ['vue-modules', 'vue', 'normal-modules', 'normal']
types.forEach(type => {
// module.rule 可以用来匹配并配置 Loader
config.module.rule('scss')
// oneOf 用于指定嵌套规则并匹配一次
.oneOf(type)
// 创建规则
.use('style-resource').loader('style-resources-loader')
// 传递选项参数,详细配置请查阅 style-resources-loader 文档
.options({
patterns: [
path.resolve(__dirname, 'src/style/variables/*.scss')
]
})
})
}
}

​ 解析下这段代码,我们的需求是对 SCSS 的规则里,添加 style-resources-loader ,所以需要找到生成默认配置的。这段例子具有很多有意思的地方,先从审查命令说起:

1
vue inspect module.rules > output.js

​ 通过该命令,可以方便地将 解析出来的 webpack 配置、包括链式访问规则和插件 的提示重定向到 output.js 文件,而这里我们只需要查看 Rules。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// output.js
{
...
/* config.module.rule('scss') */
{
test: /\.scss$/,
oneOf: [
/* config.module.rule('scss').oneOf('vue-modules') */
{
resourceQuery: /module/,
use: [
/* config.module.rule('scss').oneOf('vue-modules').use('vue-style-loader') */
{
loader: 'vue-style-loader',
options: {
...
}
},
/* config.module.rule('scss').oneOf('vue-modules').use('css-loader') */
{
loader: 'css-loader',
options: {
...
}
},
/* config.module.rule('scss').oneOf('vue-modules').use('postcss-loader') */
{
loader: 'postcss-loader',
options: {
...
}
},
/* config.module.rule('scss').oneOf('vue-modules').use('sass-loader') */
{
loader: 'sass-loader',
options: {
...
}
}
]
},
/* config.module.rule('scss').oneOf('vue') */
{...},
/* config.module.rule('scss').oneOf('normal-modules') */
{...},
/* config.module.rule('scss').oneOf('normal') */
{...}
]
},
...
}

​ 在脚手架默认配置,对 SCSS 的处理规则里会对 vue-modules、vue、normal-modules、normal 四种类型依次进行规则匹配,当规则匹配时只使用第一个匹配规则。在每一个嵌套的匹配规则中,再依次使用 vue-style-loader、css-loader、postcss-loader、sass-loader 等 Loader。

​ 将示例代码引入后,再次审查配置,应该会在发现在 use 选项的队列中,新增了一个 Loader:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/* config.module.rule('scss') */
{
test: /\.scss$/,
oneOf: [
/* config.module.rule('scss').oneOf('vue-modules') */
{
resourceQuery: /module/,
use: [
/* config.module.rule('scss').oneOf('vue-modules').use('vue-style-loader') */
{...},
/* config.module.rule('scss').oneOf('vue-modules').use('css-loader') */
{...},
/* config.module.rule('scss').oneOf('vue-modules').use('postcss-loader') */
{...},
/* config.module.rule('scss').oneOf('vue-modules').use('sass-loader') */
{...},
/* 新增的配置 */
/* config.module.rule('scss').oneOf('vue-modules').use('style-resource') */
{
loader: 'style-resources-loader',
options: {...}
}
},
...
]
}

​ 官方文档中,同时还提到了 vue-cli-plugin-style-resources-loader ,这是一个标准的 Vue-CLI3 插件。

但是在官方文档中,这种方式配置却是不被推荐的。chainWebpack 选项提供了细颗粒度修改配置的方式,但是对于样式的规则来说,本身默认生成的嵌套规则就已经达到了 4*3 种,通过以上这种方式无疑会增加了配置的复杂度、耦合度,容易造成疏漏、非常不利于维护:

对于 CSS 相关 loader 来说,我们推荐使用 css.loaderOptions 而不是直接链式指定 loader。这是因为每种 CSS 文件类型都有多个规则,而 css.loaderOptions 可以确保你通过一个地方影响所有的规则。